package com.boe.hbd.client.persistence;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import org.json.JSONObject;

import android.content.ContentValues;
import android.database.Cursor;
import android.util.Log;

import com.boe.hbd.client.annotation.Entity;
import com.boe.hbd.client.annotation.EntityField;
import com.boe.hbd.client.utils.Assert;

public class EntityMeta {

	private final static String TAG = "EntityMeta";

	private String tableName = null;
	private Map<String, String> columnNameOfFields = new HashMap<String, String>();
	private Map<String, String> fieldNameOfColumns = new HashMap<String, String>();
	private Map<String, Method> getters = new HashMap<String, Method>();
	private Map<String, Method> setters = new HashMap<String, Method>();
	private Class<?> typeOfEntity;

	private static final Map<Class<?>,EntityMeta> cachedMetas = new HashMap<Class<?>,EntityMeta>();
	
	public static EntityMeta getInstance(Class<?> typeOfEntity) {
		synchronized (cachedMetas) {
			if (!cachedMetas.containsKey(typeOfEntity)) {
				cachedMetas.put(typeOfEntity, new EntityMeta(typeOfEntity));
			}
		}
		return cachedMetas.get(typeOfEntity);
	}

	private EntityMeta(Class<?> typeOfEntity) {
		this.typeOfEntity = typeOfEntity;
		// STEP 1: parsing the tableName
		Entity entity = typeOfEntity.getAnnotation(Entity.class);
		if (entity != null) {
			tableName = entity.table();
		}
		Assert.notNull(tableName, typeOfEntity.getName() + " need be annotated with " + Entity.class.getName());
		// STEP 2: parsing the field to column mapping
		while (!typeOfEntity.equals(Object.class)) {
			for (Field field : typeOfEntity.getDeclaredFields()) {
				if (field.isAnnotationPresent(EntityField.class)) {
					String column = field.getAnnotation(EntityField.class).column();
					Assert.notNull(column, typeOfEntity.getName() + "." + field.getName() + " need be annotated with " + EntityField.class.getName());
					columnNameOfFields.put(field.getName(), column);
					fieldNameOfColumns.put(column, field.getName());
					String suffix = field.getName().substring(0, 1).toUpperCase() + field.getName().substring(1);
					try {
						getters.put(field.getName(), typeOfEntity.getDeclaredMethod("get" + suffix, new Class[] {}));
						setters.put(field.getName(), typeOfEntity.getDeclaredMethod("set" + suffix, new Class[] { field.getType() }));
					} catch (Throwable e) {
						Log.e(TAG, e.getMessage());
					}
				}
			}
			typeOfEntity = typeOfEntity.getSuperclass();
		}
	}

	public String getTableName() {
		return tableName;
	}

	public String getString(Cursor cursor , String columnName , int colIdx ) {
		Object val = null ;
		String fieldName = fieldNameOfColumns.get(columnName);
		if (fieldName != null) {
			Method getter = getters.get(fieldName);
			String typeName = getter.getReturnType().getName();
			if (typeName.equals("int") || typeName.equals(Integer.class.getName())) {
				val = cursor.getInt(colIdx);
			} else if (typeName.equals("long") || typeName.equals(Long.class.getName())) {
				val = cursor.getLong(colIdx);
			} else if (typeName.equals("double") || typeName.equals(Double.class.getName())) {
				val = cursor.getDouble(colIdx);
			} else {
				val = cursor.getString(colIdx);
			}
		}
		return val == null ? null : val.toString() ;
	}
	
	public <T> T toEntity(Map<String, String> map) {
		Assert.notNull(map, "map can not be null");
		try {
			JSONObject jsonObj = new JSONObject(new JSONObject(map).toString());
			@SuppressWarnings("unchecked")
			T entity = (T) typeOfEntity.newInstance();
			for (Map.Entry<String, String> pair : map.entrySet()) {
				String colName = pair.getKey();
				if (pair.getValue() != null && !pair.getValue().equals("")) {
					String fName = fieldNameOfColumns.get(colName);
					if (fName != null) {
						Object val = null;
						Method setter = setters.get(fName);
						String typeName = setter.getParameterTypes()[0].getName();
						if (typeName.equals("int") || typeName.equals(Integer.class.getName())) {
							val = jsonObj.getInt(colName);
						} else if (typeName.equals("long") || typeName.equals(Long.class.getName())) {
							val = jsonObj.getLong(colName);
						} else if (typeName.equals("double") || typeName.equals(Double.class.getName())) {
							val = jsonObj.getDouble(colName);
						} else if (typeName.equals("java.lang.String")) {
							val = jsonObj.getString(colName);
						}
						setter.invoke(entity, new Object[] { val });
					}
				}
			}
			return entity;
		} catch (Throwable e) {
			Log.e(TAG, e.getMessage());
		}
		return null;
	}

	public HashMap<String,Object> toHashMap(Object instOfEntity, boolean emptyNullField) {
		Assert.notNull(instOfEntity, "instOfEntity can not be null");
		Assert.isAssignable(typeOfEntity, instOfEntity.getClass(), instOfEntity.getClass().getName() + " not asignable to " + typeOfEntity.getName());
		HashMap<String,Object> values = new HashMap<String,Object>();
		for (Map.Entry<String, String> pair : columnNameOfFields.entrySet()) {
			String fieldName = pair.getKey();
			String colName = pair.getValue();
			Method getter = getters.get(fieldName);
			try {
				Object val = getter.invoke(instOfEntity, new Object[] {});
				if (val != null) {
					values.put(colName, val.toString());
				} else if (emptyNullField) {
					values.put(colName, "");
				}
			} catch (Throwable e) {
				Log.e(TAG, e.getMessage());
			}
		}
		return values ;
	}
	
	public ContentValues toContentValues(Object instOfEntity, boolean emptyNullField) {
		ContentValues cValues = new ContentValues();
		HashMap<String,Object> values = toHashMap(instOfEntity , emptyNullField);
		for (Map.Entry<String, Object> pair: values.entrySet()) {
			cValues.put(pair.getKey(), pair.getValue() == null ? null : pair.getValue().toString());
		}
		return cValues;
	}
	
	public Object get(Object instOfEntity , String fieldName) {
		Object val = null ;
		Method getter = getters.get(fieldName);
		try {
			val = getter.invoke(instOfEntity, new Object[] {});
		} catch (Throwable e) {
			Log.e(TAG, e.getMessage());
		}
		return val;
	}
	
	public Set<java.util.Map.Entry<String, Object>> entrySet(Object instOfEntity){ 
	    return toHashMap(instOfEntity,false).entrySet() ;	
	}
	
	public Collection<Object> values(Object instOfEntity) {
		return toHashMap(instOfEntity,false).values();
	}
	
	public boolean containsKey(Object key) {
		return columnNameOfFields.containsKey(key);
	}
	
	public int size() {
		return columnNameOfFields.size();
	}
	
	public Set<String> keySet() {
	    return columnNameOfFields.keySet();
    }
}
